package spreadsheet;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

import abtractTokens.CellToken;
import abtractTokens.LiteralToken;
import abtractTokens.OperatorToken;
import abtractTokens.Token;
import abtractTokens.TokenEnum;

/**
 * Keeps a formula and a collection of dependencies on other cells.
 * 
 * @author James Marquardt
 */

public class Cell {

	/**
	 * A text field to handle changes to the text displayed for this cell.
	 */
	private JTextField my_text_field;
	/**
	 * Map of all other cells that the value of this cell depends on.
	 */
	private Map<CellToken, Cell> my_depends_on_map;
	/**
	 * List of all other cells that dpends on the value of this cell.
	 */
	private Set<Cell> my_depends_on_me_set;
	/**
	 * The formula of this cell.
	 */
	private String my_formula;
	/**
	 * The value that will be displayed in this cell (Double or error).
	 */
	private String my_value;
	/**
	 * Row of this cell.
	 */
	private int my_row;
	/**
	 * Column of this cell.
	 */
	private int my_column;
	/**
	 * The spreadsheet that this cell is in.
	 */
	private Spreadsheet my_spreadsheet;

	/**
	 * Constructor for a cell object.
	 * 
	 * @param the_row The cell's row.
	 * @param the_column The cell's column.
	 * @param the_spreadsheet The spreadsheet this cell is in.
	 */
	public Cell(final int the_row, final int the_column, 
			final Spreadsheet the_spreadsheet) {

		my_depends_on_map = new HashMap<CellToken, Cell>();
		my_depends_on_me_set = new HashSet<Cell>();
		my_formula = null;
		my_value = null;
		my_row = the_row;
		my_column = the_column;
		my_spreadsheet = the_spreadsheet;
		my_text_field = null;

	}
	
	/**
	 * Sets the text of  this cell's text field.
	 * 
	 * @param the_text The text of this cell.
	 */
	public void setTextField(final JTextField the_text) {
		
		my_text_field = the_text;
		
	}
	
	/**
	 * Gets the text field of this cell.
	 * 
	 * @return The text field of this cell.
	 */
	public JTextField getTextField() {
		
		return my_text_field;
		
	}
	 
	/**
	 * Resets all variable fields of the cell.
	 */
	public void resetCell() {
		
		my_depends_on_map.clear();
		my_depends_on_me_set.clear();
		my_formula = null;
		my_value = null;
		
	}
	
	/**
	 * Sets the output string value of the cell when value is in String form (error).
	 * 
	 * @param the_value The evaluated value.
	 */
	public void setCellValue(final String the_value) {

		my_value = the_value;

	}

	/**
	 * Sets the output string value of the cell when value is a number.
	 * 
	 * @param the_value The evaluated value.
	 */
	public void setCellValue(final double the_value) {

		my_value = Double.toString(the_value);

	}

	/**
	 * Sets the cell formula.
	 * 
	 * @param the_formula The new formula.
	 */
	public void setFormula(final String the_formula) {

		my_formula = the_formula;

	}

	/**
	 * Gets the value of this cell.
	 * 
	 * @return The value in my_value.
	 */
	public String getCellValue() {

		return my_value;

	}

	/**
	 * Gets the formula in this cell.
	 * 
	 * @return The formula of this cell.
	 */
	public String getFormula() {

		return my_formula;

	}

	/**
	 * Evaluates and sets the value of the cell.
	 */
	public void evaluateCell() {
	
		Stack<Token> token_stack = new Stack<Token>();

		Stack<Token> add_stack = new Stack<Token>();

		try {
			token_stack = tokenize(my_formula);

			//Check if the returned stack was empty. If so, formula was empty.
			if(token_stack.isEmpty()) {

				setCellValue(null);
			
			//Just one LiteralToken. Easy evaluate.
			} else if(token_stack.size() == 1 && 
					token_stack.peek().getType() == TokenEnum.LITERAL) {
				
				LiteralToken val = (LiteralToken) token_stack.pop();
				setCellValue(val.toString());
			
			//Just one CellToken. Easy evaluate.
			} else if(token_stack.size() == 1 &&
					token_stack.peek().getType() == TokenEnum.CELL) {
				
				CellToken cell_tok = (CellToken) token_stack.pop();
				Cell cell = my_depends_on_map.get(cell_tok);
				
				//We want to be able to evaluate empty cells, so treat null cells as having
				//a value of zero.
				if(cell.getCellValue() == null) {

					setCellValue("0");
					
				} else { setCellValue(cell.getCellValue()); }
				
				//Check for cycles and handle dependencies.
				for (Cell i : my_depends_on_me_set) {
					
					my_spreadsheet.checkCycles(i);
					
					i.evaluateCell();
					
				}
				
			} else {
				
				//Pass 1: Evaluate exponents, everything else gets pushed onto other stack.
				while(token_stack.size() != 1) {

					Token first = token_stack.pop();
					OperatorToken op = (OperatorToken) token_stack.pop();

					if(op.getOperator() == '*' || op.getOperator() == '/' ||
							op.getOperator() == '+' || op.getOperator() == '-') {

						add_stack.push(first);
						add_stack.push(op);

					} else if(op.getOperator() == '^') {
												
						Token second = token_stack.pop();

						LiteralToken eval_tok = evaluateTokens(first, 
								op, second);

						token_stack.push(eval_tok);

					} 			

				}
				
				add_stack.push(token_stack.pop());
				
				//Pass 2: Evaluate * and /, push everything else onto original stack.
				while(add_stack.size() != 1) {
	
					Token first = add_stack.pop();
					OperatorToken op = (OperatorToken) add_stack.pop();

					if(op.getOperator() == '+' || op.getOperator() == '-') {

						token_stack.push(first);
						token_stack.push(op);

					} else if(op.getOperator() == '*' || op.getOperator() == '/') {

						Token second = add_stack.pop();

						LiteralToken eval_tok = evaluateTokens(first, 
								op, second);

						add_stack.push(eval_tok);

					} 			

				}

				token_stack.push(add_stack.pop());

				//Evaluate the low priority operation stack.
				while(token_stack.size() != 1) {

					token_stack.push(evaluateTokens(token_stack.pop(), 
							(OperatorToken) token_stack.pop(), token_stack.pop()));


				}

				LiteralToken value_token = (LiteralToken) token_stack.pop();

				setCellValue(value_token.toString());
				
				//If there are any cells depending on this cell, there is a dependency
				//graph. Go through each cell in the graph to find the degree 0 cell and
				//evaluate all cells accordingly.
				//This procedure gets rid of need for an explicit topological sort.
				if(!my_depends_on_me_set.isEmpty()) {
					
					for (Cell i : my_depends_on_me_set) {
						
						my_spreadsheet.checkCycles(i);
						
						i.evaluateCell();

					}
					
				}
	
			}
		
		//This will catch strings being passed into Integer.decode() in evaluateTokens.
		} catch (NumberFormatException e) {
			
			JOptionPane.showMessageDialog(new JFrame(), "One argument does not" +
					" evaluate to a number" , "Error",
					JOptionPane.ERROR_MESSAGE);
			setCellValue("error");
			
		//All other exceptions
		} catch (Exception e) {

			//An exception thrown while formula is surrounded by quotation marks will be
			//displayed in the cell as a string.
			if(my_formula != null) {
				if(my_formula.charAt(0) == '"' && 
					my_formula.charAt(my_formula.length()-1) == '"') {
				
					String temp_string = my_formula.replace('"', '\0');
					temp_string = temp_string.trim();
					setCellValue(temp_string);
	
				}
				
				//All errors handled here.
				else {
							
					JOptionPane.showMessageDialog(new JFrame(), e.getMessage(), "Error",
								JOptionPane.ERROR_MESSAGE);
					setCellValue("error");
			
				}
			}

		}

	}
	
	/**
	 * Returns the set of cells that depends on this cell.
	 * 
	 * @return The set of cells that depend on this cell.
	 */
	public Set<Cell> getDependsOnMeSet() {
		
		return my_depends_on_me_set;
		
	}
	
	/**
	 * Returns the map of all cells that this cell depends on.
	 * 
	 * @return The map of cells that this cell depends on.
	 */
	public Map<CellToken, Cell> getDependsOnMap() {
		
		return my_depends_on_map;
		
	}
	
	/**
	 * Getter for the row of the cell.
	 * 
	 * @return The row of the cell.
	 */
	public int getMyRow() {
		
		return my_row;
		
	}
	
	/**
	 * Getter for the column of the cell.
	 * 
	 * @return The column of the cell.
	 */
	public int getMyColumn() {
		
		return my_column;
		
	}

	/**
	 * Creates a stack of Tokens based on the formula.
	 * 
	 * @param the_formula The formula in this cell.
	 * @return The stack of tokens generated.
	 * @throws Exception for invalid formulas.
	 */
	public Stack<Token> tokenize(final String the_formula) throws Exception {
		
		my_depends_on_map.clear();
		
		//my_depends_on_me_set.clear();
		
		//A stack of tokens to fill and return.
		Stack<Token> ret_stack = new Stack<Token>();

		//If the formula is empty or null, no need to tokenize.
		if(my_formula == "" || my_formula == null) {

			return ret_stack;

		} else {

			//A placeholder for the formula characters
			int index = 0;

			//Examine every character for indicators that they may be tokens.
			while(index < the_formula.length()) {
							
				//This may be an operator token.
				if(checkIsOperator(the_formula.charAt(index))) {

					//Operators cannot be at beginning or end of formula, or
					//be next to each other
					/**This does not take into account parentheses. Fix**/
					
					if(index == 0 || ret_stack.peek().getType() == TokenEnum.OPERATOR ||
							index == the_formula.length()-1) {

						throw new Exception("Invalid Formula (operator placement)");

					}

					ret_stack.push(new OperatorToken(
							Character.toString(the_formula.charAt(index))));

					index++;

					//This may be a literaltoken.
				} else if(checkIsNumber(the_formula.charAt(index))) {

					String lit_str = "";

					//Make sure all digits of literaltoken are added.
					while(index < the_formula.length()){

						if(checkIsNumber(the_formula.charAt(index))) {

							lit_str += the_formula.charAt(index);

							index++;

						} else { break; }

					}

					ret_stack.push(new LiteralToken(lit_str));

					//This may be a cell token.
				} else if(checkIsAlphabetical(the_formula.charAt(index))) {

					//CellToken can only follow a OperatorToken
					if(index != 0 && ret_stack.peek().getType() != TokenEnum.OPERATOR) {

						throw new Exception("Invalid Formula (CellToken following CellToken " +
								"or LiteralToken)");

					}

					String cell_str = "";

					//Add all letters in cell token.
					while(index < the_formula.length()){

						if(checkIsAlphabetical(the_formula.charAt(index))) {

							cell_str += the_formula.charAt(index);

							index++;

						} else { break; }

					}

					//Cannot have a letter by itself at the end of the formula.
					if(index == the_formula.length()) {

						throw new Exception("Invalid Formula (incomplete CellToken)");

					}

					//Letter portion of cell token must be followed by number.
					if(!checkIsNumber(the_formula.charAt(index))) {

						throw new Exception("Invalid Formula (incomplete CellToken)");

					}

					//Add all numbers in cell token.
					while(index < the_formula.length()){

						if(checkIsNumber(the_formula.charAt(index))) {

							cell_str += the_formula.charAt(index);

							index++;

						} else { break; }

					}

					//Create new cell token.
					CellToken cell_tok = new CellToken(cell_str);

					//Map celltoken to appropriate cell in spreadsheet.
					my_depends_on_map.put(cell_tok, my_spreadsheet.getCell(cell_tok.getRow(),
							cell_tok.getColumn()));
					
					//Update list in specified cell to show that this cell depends on it.
					//(for finding in degree-0)
					my_spreadsheet.getCell(cell_tok.getRow(), 
							cell_tok.getColumn()).getDependsOnMeSet().
							add(my_spreadsheet.getCell(my_row + 1, my_column + 1));

					//Push same celltoken onto return stack.
					ret_stack.push(cell_tok);

					//Ignore whitespace
				} else if(Character.isWhitespace(the_formula.charAt(index)) ||
						the_formula.charAt(index) == '(' || 
						the_formula.charAt(index) == ')') {

					index++;

					//Do not accept special characters
				} else {

					throw new Exception("Invalid Formula (invalid character in formula)");

				}

			}

			return ret_stack;
		}
	}

	/**
	 * Checks whether a character is A-Z (uppercase or lowercase).
	 * @param the_char The character being checked.
	 * @return Whether the_char is A-Z.
	 */
	private boolean checkIsAlphabetical(final char the_char) {

		boolean ret_bool = false;

		if(('a' <= the_char && the_char <= 'z') || ('A' <= the_char && the_char <= 'Z')) {

			ret_bool = true;

		}

		return ret_bool;

	}

	/**
	 * Checks whether a char is an operator (+, -, *, /)
	 * @param the_char The character being checked.
	 * @return Whether the_char is an operator.
	 */
	private boolean checkIsOperator(final char the_char) {

		boolean ret_bool = false;

		if(the_char == '*' || the_char == '/' || the_char == '+' || the_char == '-' ||
				the_char == '^') {

			ret_bool = true;

		}

		return ret_bool;
	}

	/**
	 * Checks whether a char is a number (0-9)
	 * @param the_char The character being checked.
	 * @return Whether the_char is a number.
	 */
	private boolean checkIsNumber(final char the_char) {

		boolean ret_bool = false;

		if('0' <= the_char && the_char <= '9') {

			ret_bool = true;

		}

		return ret_bool;

	}

	/**
	 * Evaluates a trio of Tokens.
	 * 
	 * @param the_token_one The first literal in the trio.
	 * @param the_op The operator token.
	 * @param the_token_two The second literal in the trio.
	 * @return A LiteralToken containint the evaluated value.
	 */
	private LiteralToken evaluateTokens(final Token the_token_one, 
			final OperatorToken the_op, final Token the_token_two) {
		
		int ret_val = 0;
		int val_one = 0;
		int val_two = 0;

		//Get value from first operand token, whether it is literal or cell
		if(the_token_one.getType() == TokenEnum.LITERAL) {

			val_one = Integer.decode(the_token_one.toString());

		} else if(the_token_one.getType() == TokenEnum.CELL) {

			Cell cell = my_depends_on_map.get(the_token_one);
			
			//To evaluate null value cells, treat value as 0.
			if(cell.getCellValue() == null) {
				
				val_one = 0;
				
			} else { val_one = Integer.decode(cell.getCellValue()); }
		
		}

		//Get value from second operand token, whether it is literal or cell
		if(the_token_two.getType() == TokenEnum.LITERAL) {

			val_two = Integer.decode(the_token_two.toString());

		} else if(the_token_two.getType() == TokenEnum.CELL) {

			Cell cell = my_depends_on_map.get(the_token_two);
			
			//To evaluate null value cells, treat value as 0.
			if(cell.getCellValue() == null) {
				
				val_two = 0;
				
			} else { val_two = Integer.decode(cell.getCellValue()); }	

		}

		if(the_op.getOperator() == '+') {

			ret_val = val_one + val_two;

		} else if(the_op.getOperator() == '-') {

			ret_val = val_two - val_one;

		} else if(the_op.getOperator() == '*') {

			ret_val = val_two * val_one;

		} else if(the_op.getOperator() == '/') {

			ret_val = val_one / val_two;

		} else if(the_op.getOperator() == '^') {
			
			if(val_two == 0) {
			
				ret_val = 0;
			
			} else if (val_one == 0) {
			
				ret_val = 1;
			
			} else {
			
				ret_val = (int) Math.pow(val_two, val_one);
			
			}
		}

		return new LiteralToken(Integer.toString(ret_val));

	}

}
